#pragma once

#include "../dependencies/mysql/mysql.h"
#include <string>
#include <cstring>
#include <vector>
#include <stdexcept>

#include "exception/SQLException.hpp"
#include "../utility/format/Format.hpp"

namespace mysql {

    class MysqlResult {

    public:

        /**
          * @brief  构造函数接收预处理语句对象
          * @note   请勿拷贝该对象，浅拷贝可能会引发多重释放问题
          */
        explicit MysqlResult(MYSQL_STMT *stmt);

        /**
          * @brief  析构函数
          * @note   该函数将销毁结果集所有数据成员
          */
        ~MysqlResult();

        /**
          * @brief  调整结果行指向下一行
          * @retval 下一行是否为空
          */
        bool next();

        /**
          * @brief  获取指定列的字符串数据
          * @param  定列的索引
          * @retval 字符串数据
          */
        std::string getString(int column) const;

        /**
          * @brief  获取指定列的整形数据
          * @param  column 指定列
          * @retval 整形数据
          */
        int getInt(int column) const;

        /**
          * @brief  检查指定列是否为空值
          * @param  column 指定列的索引
          * @retval 是否为空值
          */
        bool isNull(int column) const { return *bind_[column].is_null == 1; }

        /**
          * @brief  获取列名
          * @param  column 列索引
          * @retval 列名
          */
        std::string getColumnName(int column) const;

        /**
          * @brief  获取结果集的列数
          * @retval 结果集的列数
          */
        unsigned long getColumnCount() const { return mysql_num_fields(result_); }

        /**
          * @brief  获取结果集的行数
          * @retval 结果集的行数
          */
        unsigned long getRowCount() const { return mysql_stmt_num_rows(stmt_); }

    private:

        /**
          * @brief  将MYSQL_TIME时间类型转换为字符串
          * @note   默认按照xxxx-xx-xx xx:xx:xx格式进行转换
          * @param  MYSQL_TIME结构体指针
          * @retval 转换后的字符串
          */
        static std::string timeToString(MYSQL_TIME *);

        // 预处理语句对象
        MYSQL_STMT *stmt_;

        // 结果元数据
        MYSQL_RES *result_;

        // 当前行数据
        MYSQL_BIND *bind_;

    };

    MysqlResult::MysqlResult(MYSQL_STMT *stmt) :
            stmt_(stmt), result_(nullptr), bind_(nullptr) {
        if (mysql_stmt_store_result(stmt)) {
            if (mysql_stmt_errno(stmt) != 0) {
                throw SQLException("Failed to store result set", mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
            }
        }
        result_ = mysql_stmt_result_metadata(stmt);
        if (!result_) {
            if (mysql_stmt_errno(stmt) != 0) {
                throw SQLException("Failed to get result metadata", mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
            }
        }
        unsigned long num_fields = mysql_num_fields(result_);
        bind_ = new MYSQL_BIND[num_fields];
        memset(bind_, 0, sizeof(MYSQL_BIND) * num_fields);
        MYSQL_FIELD *fields = mysql_fetch_fields(result_);
        for (unsigned long i = 0; i < num_fields; ++i) {
            bind_[i].buffer_type = fields[i].type;
            bind_[i].is_null = new char;
            switch (fields[i].type) {
                case MYSQL_TYPE_STRING:
                case MYSQL_TYPE_VAR_STRING:
                    bind_[i].buffer = new char[fields[i].length + 1];
                    bind_[i].buffer_length = fields[i].length + 1;
                    break;
                case MYSQL_TYPE_LONG:
                    bind_[i].buffer = new int;
                    break;
                case MYSQL_TYPE_TIMESTAMP:
                case MYSQL_TYPE_DATETIME:
                    bind_[i].buffer = new MYSQL_TIME;
                    break;
                default:
                    throw SQLException("Unsupported column type", 0);
            }
        }
        if (mysql_stmt_bind_result(stmt_, bind_)) {
            throw SQLException("Failed to bind result", mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
        }
    }

    MysqlResult::~MysqlResult() {
        if (result_ != nullptr) {
            mysql_free_result(result_);
        }
        if (bind_ != nullptr) {
            unsigned long num_fields = mysql_num_fields(result_);
            for (unsigned long i = 0; i < num_fields; ++i) {
                delete static_cast<char *>(bind_[i].is_null);
                switch (bind_[i].buffer_type) {
                    case MYSQL_TYPE_STRING:
                    case MYSQL_TYPE_VAR_STRING:
                        delete[] static_cast<char *>(bind_[i].buffer);
                        break;
                    case MYSQL_TYPE_LONG:
                        delete static_cast<int *>(bind_[i].buffer);
                        break;
                    case MYSQL_TYPE_TIMESTAMP:
                    case MYSQL_TYPE_DATETIME:
                        delete static_cast<MYSQL_TIME *>(bind_[i].buffer);
                        break;
                }
            }
            delete[] bind_;
        }
    }

    bool MysqlResult::next() {
        return mysql_stmt_fetch(stmt_) == 0;
    }

    std::string MysqlResult::getString(int column) const {
        if (bind_ == nullptr || column < 0 || column >= mysql_num_fields(result_)) {
            throw SQLException("Invalid column index", 0);
        }
        if (isNull(column)) {
            return "";
        }
        enum_field_types type = bind_[column].buffer_type;
        if (type == MYSQL_TYPE_STRING || type == MYSQL_TYPE_VAR_STRING) {
            return static_cast<char *>(bind_[column].buffer);
        } else if (type == MYSQL_TYPE_LONG) {
            return std::to_string(*static_cast<int *>(bind_[column].buffer));
        } else if (type == MYSQL_TYPE_DATETIME || type == MYSQL_TYPE_TIMESTAMP) {
            return timeToString(static_cast<MYSQL_TIME *>(bind_[column].buffer));
        }
        throw SQLException("Unknown type", 0);
    }

    int MysqlResult::getInt(int column) const {
        if (bind_ == nullptr || column < 0 || column >= mysql_num_fields(result_)) {
            throw SQLException("Invalid column index", 0);
        }
        if (isNull(column)) {
            return 0;
        }
        if (bind_[column].buffer_type != MYSQL_TYPE_LONG) {
            throw SQLException("Not int type", 0);
        }
        return *static_cast<int *>(bind_[column].buffer);
    }

    std::string MysqlResult::getColumnName(int column) const {
        if (result_ == nullptr || column < 0 || column >= mysql_num_fields(result_)) {
            throw SQLException("Invalid column index", 0);
        }
        MYSQL_FIELD *field = mysql_fetch_field_direct(result_, column);
        return field ? field->name : "";
    }

    std::string MysqlResult::timeToString(MYSQL_TIME *time) {
        return format::Format("{0}-{1}-{2} {3}:{4}:{5}", time->year, time->month, time->day, time->hour, time->minute,
                              time->second);
    }

}